Skip to content

Add an sensor for the tonal palette for dynamic colors - #7319

Open
kcoppock wants to merge 3 commits into
home-assistant:mainfrom
kcoppock:tonal_palette_sensor
Open

Add an sensor for the tonal palette for dynamic colors#7319
kcoppock wants to merge 3 commits into
home-assistant:mainfrom
kcoppock:tonal_palette_sensor

Conversation

@kcoppock

@kcoppock kcoppock commented Aug 10, 2026

Copy link
Copy Markdown

Summary

This adds an additional sensor to the current Dynamic Color sensor for the tonal palette variant. In conjunction with the seed color, this is used to determine the generated color palette.

This combination allows for shared color definitions between Home Assistant and the phone's theme. The motivation here was that I was creating a theme that would use the Material Color Utilities library to create a color palette that would align with the phone's theme. This allows the app to blend in with native applications more effectively.

The seed color (already exposed as a sensor) is not entirely sufficient, since the generated palette can vary widely based on the selected theme style.

Checklist

  • New or updated tests have been added to cover the changes following the testing guidelines.
  • The code follows the project's code style and best_practices.
  • The changes have been thoroughly tested, and edge cases have been considered.
  • Changes are backward compatible whenever feasible. Any breaking changes are documented in the changelog for users and/or in the code for developers depending on the relevance.
  • I have read the Open Home Foundation AI Policy.

Select exactly one option that describes AI usage in this contribution:

  • I have not used AI for this contribution.
  • AI assistance was used for this contribution.
  • AI fully generated the code for this contribution, but I've reviewed and understood it before submitting and will respond without AI during review.

Screenshots

as_sensor_entity sensor_enabled sensor_in_list

Link to pull request in documentation repositories

User Documentation: home-assistant/companion.home-assistant#1403

Any other notes

@home-assistant home-assistant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @kcoppock

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the existing Dynamic Color (accent color) sensor to expose the system “theme style” (tonal palette variant) alongside the already-reported seed/accent color, enabling Home Assistant themes to generate palettes consistent with the phone’s dynamic color style.

Changes:

  • Read android.theme.customization.theme_style from Settings.Secure theme overlay JSON and publish it as a new variant attribute on the accent color sensor.
  • Add Robolectric/Hilt tests covering sensor availability, state updates, RGB attribute output, and the new variant attribute behavior (including malformed input).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
app/src/main/kotlin/io/homeassistant/companion/android/sensors/DynamicColorSensorManager.kt Adds reading/parsing of theme style from system settings and publishes it as a new sensor attribute.
app/src/test/kotlin/io/homeassistant/companion/android/sensors/DynamicColorSensorManagerTest.kt Adds unit tests validating dynamic color sensor behavior and the new variant attribute.
Suppressed comments (1)

app/src/main/kotlin/io/homeassistant/companion/android/sensors/DynamicColorSensorManager.kt:103

  • PR description mentions adding an additional sensor, but the code change here adds a new attribute ("variant") to the existing dynamic color sensor update. If no separate sensor was intended, consider updating the PR summary/description to match the implemented behavior to avoid confusion for reviewers and docs.
            mapOf(
                "rgb_color" to listOf(accent.red, accent.green, accent.blue),
                "variant" to variantThemeStyle.orEmpty(),
            ),

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@jpelgrom

Copy link
Copy Markdown
Member

Thanks for the suggestion.

The use of attributes is strongly discouraged in Home Assistant nowadays. As this doesn't describe the color, it could be a full sensor instead, I think. We can then also provide a proper description, and send all possible values to the server, to improve the user experience.

Manually reading JSON from undocumented system settings feels like it could be error-prone, especially as multiple manufacturers implement Dynamic color differently. Do you have any additional sources or reports that indicate this is stable and works on a wide variety of devices? What about simply exposing all theme colors? (Doing a quick search in Android source code I can't find a documented API or attribute for the palette enum either.)

@kcoppock

kcoppock commented Aug 11, 2026

Copy link
Copy Markdown
Author

Thanks for the suggestion.

The use of attributes is strongly discouraged in Home Assistant nowadays. As this doesn't describe the color, it could be a full sensor instead, I think. We can then also provide a proper description, and send all possible values to the server, to improve the user experience.

Manually reading JSON from undocumented system settings feels like it could be error-prone, especially as multiple manufacturers implement Dynamic color differently. Do you have any additional sources or reports that indicate this is stable and works on a wide variety of devices? What about simply exposing all theme colors? (Doing a quick search in Android source code I can't find a documented API or attribute for the palette enum either.)

Ha, I actually originally had it as an additional sensor and then thought the attribute approach was the way to go. Happy to change that back, but want to get your input first on the rest of your comment.

I would prefer to use a declared constant, but Settings.Secure.THEME_OVERLAY_CUSTOMIZATION_PACKAGES is marked as a @SystemApi so is not exposed in the SDK. It is, however, required as part of CTS testing as described in these docs:

Note: You aren't required to expose ALL of the new theme styles in your UI, and can decide which to expose based on your user/brand needs. However, dynamic color palettes MUST be generated through one of these theme styles as CTS-tested by SystemPaletteTest#testThemeStyles and mandated by CDD.

Tests are here.

I wouldn't recommend treating it as an enum with a fixed set of values, even if the CTS tested values are all currently enumerated. It is possible that future versions of Android (or other OEMs) would expand on this. Anyone reading from the sensor would be expected to handle unknown values (at least the way I envision it).

Thanks for the review! Let me know your thoughts and if you think it's a practical way forward I'll change to an additional sensor.

Edit: As far as surfacing all colors, that is possible but would be quite expansive (currently 65 colors).

@jpelgrom

Copy link
Copy Markdown
Member

It is, however, required as part of CTS testing (....) Tests are here.

Thanks for linking to the CTS test, that does give some confidence in the setting and its results. I didn't check it in the first place but indeed, it does use the same format (JSON).

I wouldn't recommend treating it as an enum with a fixed set of values

Sending all known options doesn't lock the sensor to behave like an enum and reject unknown values. However it does provide users with a nice dropdown of all known options for their automations, and generates warnings on the server if a different value is sent which allows us to discover and document them. Without it requires users to review the documentation to know what to expect at all.

I'm almost certain we'll discover more as I already have a choice of 9 palettes on my Pixel - or we discover that this value isn't detailed enough.

As far as surfacing all colors, that is possible but would be quite expansive (currently 65 colors).

OK I agree 65 sensors isn't ideal 😅

@kcoppock
kcoppock force-pushed the tonal_palette_sensor branch from 5874420 to 2aeda6f Compare August 13, 2026 20:49
@kcoppock

Copy link
Copy Markdown
Author

Sending all known options doesn't lock the sensor to behave like an enum and reject unknown values. However it does provide users with a nice dropdown of all known options for their automations, and generates warnings on the server if a different value is sent which allows us to discover and document them. Without it requires users to review the documentation to know what to expect at all.

Got it, thanks for the clarification! I've pushed the updated changes as well as new screenshots in the PR description. I updated the sensor logic to encode the list of options (let me know if I did this correctly -- I didn't see these appear as options when creating an automation, but was following what I saw for other enum-type sensors). I added a bit more safeguarding when reading / parsing the JSON data just to be sure there is specific warning messaging for various situations.

I'm almost certain we'll discover more as I already have a choice of 9 palettes on my Pixel - or we discover that this value isn't detailed enough.

Regarding this, these are actually all limited to TONAL_SPOT, EXPRESSIVE, VIBRANT, SPRITZ, and MONOCHROMATIC on Pixel (I have 12 options on Android 17 QPR2 Beta). If you're interested, here's a full dump below going through the various options, but in short, it's essentially combinations of different seeds from the wallpaper, combined with different palette generation methods. The "Other colors" section is all TONAL_SPOT with different fixed seed colors.

Wallpaper Options:

{"_applied_timestamp":1786652222932,"android.theme.customization.color_index":"1","android.theme.customization.color_source":"home_wallpaper","android.theme.customization.theme_style":"TONAL_SPOT","android.theme.customization.color_both":"1"}
{"_applied_timestamp":1786652197829,"android.theme.customization.system_palette":"333333","android.theme.customization.accent_color":"333333","android.theme.customization.color_source":"preset","android.theme.customization.color_index":"0","android.theme.customization.theme_style":"MONOCHROMATIC"}
{"_applied_timestamp":1786652258666,"android.theme.customization.color_index":"1","android.theme.customization.color_both":"1","android.theme.customization.color_source":"home_wallpaper","android.theme.customization.theme_style":"SPRITZ"}
{"_applied_timestamp":1786652269467,"android.theme.customization.color_index":"1","android.theme.customization.color_both":"1","android.theme.customization.color_source":"home_wallpaper","android.theme.customization.theme_style":"VIBRANT"}
{"_applied_timestamp":1786652285373,"android.theme.customization.color_index":"1","android.theme.customization.color_both":"1","android.theme.customization.color_source":"home_wallpaper","android.theme.customization.theme_style":"EXPRESSIVE"}
{"_applied_timestamp":1786652304826,"android.theme.customization.color_index":"2","android.theme.customization.color_both":"1","android.theme.customization.system_palette":"8794A5","android.theme.customization.color_source":"home_wallpaper","android.theme.customization.theme_style":"TONAL_SPOT"}
{"_applied_timestamp":1786652316969,"android.theme.customization.color_index":"2","android.theme.customization.color_both":"1","android.theme.customization.system_palette":"8794A5","android.theme.customization.color_source":"home_wallpaper","android.theme.customization.theme_style":"SPRITZ"}
{"_applied_timestamp":1786652329079,"android.theme.customization.color_index":"2","android.theme.customization.color_both":"1","android.theme.customization.system_palette":"8794A5","android.theme.customization.color_source":"home_wallpaper","android.theme.customization.theme_style":"VIBRANT"}
{"_applied_timestamp":1786652337195,"android.theme.customization.color_index":"2","android.theme.customization.color_both":"1","android.theme.customization.system_palette":"8794A5","android.theme.customization.color_source":"home_wallpaper","android.theme.customization.theme_style":"EXPRESSIVE"}
{"_applied_timestamp":1786652344994,"android.theme.customization.color_index":"3","android.theme.customization.color_both":"1","android.theme.customization.system_palette":"6D4347","android.theme.customization.color_source":"home_wallpaper","android.theme.customization.theme_style":"TONAL_SPOT"}
{"_applied_timestamp":1786652353316,"android.theme.customization.color_index":"3","android.theme.customization.color_both":"1","android.theme.customization.system_palette":"6D4347","android.theme.customization.color_source":"home_wallpaper","android.theme.customization.theme_style":"SPRITZ"}
{"_applied_timestamp":1786652361151,"android.theme.customization.color_index":"3","android.theme.customization.color_both":"1","android.theme.customization.system_palette":"6D4347","android.theme.customization.color_source":"home_wallpaper","android.theme.customization.theme_style":"VIBRANT"}
{"_applied_timestamp":1786652370295,"android.theme.customization.color_index":"3","android.theme.customization.color_both":"1","android.theme.customization.system_palette":"6D4347","android.theme.customization.color_source":"home_wallpaper","android.theme.customization.theme_style":"EXPRESSIVE"}

Color Options:
{"_applied_timestamp":1786652395516,"android.theme.customization.color_index":"1","android.theme.customization.system_palette":"333333","android.theme.customization.accent_color":"333333","android.theme.customization.color_source":"preset","android.theme.customization.theme_style":"MONOCHROMATIC"}
{"_applied_timestamp":1786652409727,"android.theme.customization.color_index":"2","android.theme.customization.system_palette":"1A73E8","android.theme.customization.accent_color":"1A73E8","android.theme.customization.color_source":"preset","android.theme.customization.theme_style":"TONAL_SPOT"}
{"_applied_timestamp":1786652417198,"android.theme.customization.color_index":"3","android.theme.customization.system_palette":"F94AAB","android.theme.customization.accent_color":"F94AAB","android.theme.customization.color_source":"preset","android.theme.customization.theme_style":"TONAL_SPOT"}
{"_applied_timestamp":1786652424223,"android.theme.customization.color_index":"4","android.theme.customization.system_palette":"DB372D","android.theme.customization.accent_color":"DB372D","android.theme.customization.color_source":"preset","android.theme.customization.theme_style":"TONAL_SPOT"}
{"_applied_timestamp":1786652430979,"android.theme.customization.color_index":"5","android.theme.customization.system_palette":"FF8D41","android.theme.customization.accent_color":"FF8D41","android.theme.customization.color_source":"preset","android.theme.customization.theme_style":"TONAL_SPOT"}
{"_applied_timestamp":1786652437326,"android.theme.customization.color_index":"6","android.theme.customization.system_palette":"FCBD00","android.theme.customization.accent_color":"FCBD00","android.theme.customization.color_source":"preset","android.theme.customization.theme_style":"TONAL_SPOT"}
{"_applied_timestamp":1786652447284,"android.theme.customization.color_index":"7","android.theme.customization.system_palette":"BDD100","android.theme.customization.accent_color":"BDD100","android.theme.customization.color_source":"preset","android.theme.customization.theme_style":"TONAL_SPOT"}
{"_applied_timestamp":1786652454222,"android.theme.customization.color_index":"8","android.theme.customization.system_palette":"1AA64A","android.theme.customization.accent_color":"1AA64A","android.theme.customization.color_source":"preset","android.theme.customization.theme_style":"TONAL_SPOT"}
{"_applied_timestamp":1786652461857,"android.theme.customization.color_index":"9","android.theme.customization.system_palette":"00C0AC","android.theme.customization.accent_color":"00C0AC","android.theme.customization.color_source":"preset","android.theme.customization.theme_style":"TONAL_SPOT"}
{"_applied_timestamp":1786652469610,"android.theme.customization.color_index":"10","android.theme.customization.system_palette":"00BBDF","android.theme.customization.accent_color":"00BBDF","android.theme.customization.color_source":"preset","android.theme.customization.theme_style":"TONAL_SPOT"}            
{"_applied_timestamp":1786652482967,"android.theme.customization.color_index":"11","android.theme.customization.system_palette":"3271EA","android.theme.customization.accent_color":"3271EA","android.theme.customization.color_source":"preset","android.theme.customization.theme_style":"TONAL_SPOT"}
{"_applied_timestamp":1786652490675,"android.theme.customization.color_index":"12","android.theme.customization.system_palette":"695FFF","android.theme.customization.accent_color":"695FFF","android.theme.customization.color_source":"preset","android.theme.customization.theme_style":"TONAL_SPOT"}
{"_applied_timestamp":1786652499268,"android.theme.customization.color_index":"13","android.theme.customization.system_palette":"9254EA","android.theme.customization.accent_color":"9254EA","android.theme.customization.color_source":"preset","android.theme.customization.theme_style":"TONAL_SPOT"}

@kcoppock kcoppock changed the title Add an attribute for the tonal palette for dynamic colors Add an sensor for the tonal palette for dynamic colors Aug 14, 2026
This adds an additional sensor to pair with the current Dynamic Color
sensor for the seed color. In conjunction with the seed color, a theme
style is used to determine the generated color palette.

This combination allows for shared color definitions between
Home Assistant and the phone's theme.

In the case of an error with parsing the JSON data, the state will be
rendered as UNKNOWN.
@kcoppock
kcoppock force-pushed the tonal_palette_sensor branch from 2aeda6f to e7a6d70 Compare August 14, 2026 20:12
Comment thread .idea/ktfmt.xml Outdated
@kcoppock
kcoppock force-pushed the tonal_palette_sensor branch from b92469f to c0b8e13 Compare August 22, 2026 00:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants